﻿using System;
using System.Diagnostics;

using Storm.TextEditor.Editor.Text;
using Storm.TextEditor.Parser.Objects;

namespace Storm.TextEditor.Editor.Interacting
{
	/// <summary>
	/// Represents the caret that indicates the user's current position in a document.
	/// </summary>
	public sealed class Caret
	{
		#region Members

		private bool blink          = false;
		private int  oldLogicalXPos = 0;

		private TextPoint      position = null;
		private TextEditorBase control  = null;

		#region Events

		/// <summary>
		/// Occurs when the caret's position has changed.
		/// </summary>
		public event EventHandler Change = null;

		#endregion

		#endregion

		#region Properties

		/// <summary>
		/// Gets or sets the position of the caret.
		/// </summary>
		public TextPoint Position
		{
			get { return position; }
			set
			{
				position = value;
				position.Change += new EventHandler(PositionChange);
				OnChange();
			}
		}

		/// <summary>
		/// Gets or sets whether the caret should blink.
		/// </summary>
		public bool Blink
		{
			get { return blink; }
			set { blink = value; }
		}

		/// <summary>
		/// Gets the word that the caret is placed on.
		/// This only applies if the active row is fully parsed.
		/// </summary>
		public Word CurrentWord
		{ get { return control.Document.GetWordFromPos(Position); } }

		/// <summary>
		/// Gets the row that the caret is placed on.
		/// </summary>
		public Row CurrentRow
		{ get { return control.Document[Position.Y]; } }

		/// <summary>
		/// Gets the current offset of the caret.
		/// </summary>
		public int Offset
		{
			get
			{
				if (Position.Y >= control.Document.Lines.Length)
					return 0;
				TextRange tr = new TextRange(0, 0, Position.X, Position.Y);
				int l = control.Document.GetRange(tr).Length;
				int tl = control.Document.Text.Length;
				return Math.Min(tl, l);
			}
		}

		/// <summary>
		/// Gets or sets the logical position of the caret.
		/// </summary>
		public TextPoint LogicalPosition
		{
			get
			{
				if (Position.X < 0)
					return new TextPoint(0, Position.Y);

				Row xtr = CurrentRow;
				int x = 0;
				if (xtr == null)
					return new TextPoint(0, 0);

				int max = xtr.Text.Length;
				int Padd = Math.Max(Position.X - xtr.Text.Length, 0);
				string PaddStr = new String(' ', Padd);
				string TotStr = xtr.Text + PaddStr;

				foreach (char c in TotStr.ToCharArray(0, Position.X))
				{
					if (c == '\t')
						x += control.TabSpaces - (x % control.TabSpaces);
					else
						x++;
				}

				return new TextPoint(x, Position.Y);
			}
			set
			{
				Row xtr = CurrentRow;
				int x = 0;
				int xx = 0;

				if (value.X > 0)
				{
					char[] chars = xtr.Text.ToCharArray();
					int i = 0;

					while (x < value.X)
					{
						char c;
						if (i < chars.Length)
							c = chars[i];
						else
							c = ' ';

						xx++;
						if (c == '\t')
							x += control.TabSpaces - (x % control.TabSpaces);
						else
							x++;

						i++;
					}
				}

				Position.Y = value.Y;
				Position.X = xx;
			}
		}

		#endregion

		#region Methods

		#region Public

		/// <summary>
		/// Moves the caret right one step.
		/// </summary>
		public void MoveRight(bool select)
		{
			this.CropPosition();
			this.Position.X++;

			if (this.CurrentRow.IsCollapsed == true)
			{
				if (this.Position.X > this.CurrentRow.ExpansionEndChar)
				{
					this.Position.Y = this.CurrentRow.ExpansionEndRow.Index;
					this.Position.X = this.CurrentRow.ExpansionEndRow.ExpansionStartChar;
					this.CropPosition();
				}

				this.RememberXPos();
				this.CaretMoved(select);
			}
			else
			{
				Row row = this.CurrentRow;
				if (this.Position.X > row.Text.Length && control.VirtualWhitespace == false)
				{
					if (this.Position.Y < control.Document.Count - 1)
					{
						this.MoveDown(select);
						this.Position.X = 0;
						this.CropPosition();
					}
					else
						this.CropPosition();
				}

				this.RememberXPos();
				this.CaretMoved(select);
			}
		}

		/// <summary>
		/// Moves the caret up one row.
		/// </summary>
		public void MoveUp(bool select)
		{
			this.CropPosition();
			int x = oldLogicalXPos;

			if (this.CurrentRow != null && this.CurrentRow.PreviousVisibleRow != null)
			{
				this.Position.Y = this.CurrentRow.PreviousVisibleRow.Index;
				if (this.CurrentRow.IsCollapsed == true)
					x = 0;
			}

			this.CropPosition();
			this.LogicalPosition = new TextPoint(x, this.Position.Y);
			this.CropPosition();
			this.CaretMoved(select);
		}

		/// <summary>
		/// Moves the caret up x rows.
		/// </summary>
		/// <param name="rows">Number of rows the caret should be moved up.</param>
		public void MoveUp(int rows, bool select)
		{
			this.CropPosition();
			int x = oldLogicalXPos;
			int pos = this.CurrentRow.VisibleIndex;

			pos -= rows;
			if (pos < 0)
				pos = 0;

			Row r = control.Document.VisibleRows[pos];
			pos = r.Index;

			this.Position.Y = pos;
			if (this.CurrentRow.IsCollapsed == true)
				x = 0;

			this.CropPosition();
			this.LogicalPosition = new TextPoint(x, this.Position.Y);
			this.CropPosition();
			this.CaretMoved(select);
		}

		/// <summary>
		/// Moves the caret down x rows.
		/// </summary>
		/// <param name="rows">The number of rows the caret should be moved down.</param>
		public void MoveDown(int rows, bool select)
		{
			int x = oldLogicalXPos;
			this.CropPosition();

			try
			{
				int pos = this.CurrentRow.VisibleIndex;
				pos += rows;
				if (pos > control.Document.VisibleRows.Count - 1)
					pos = control.Document.VisibleRows.Count - 1;

				Row row = control.Document.VisibleRows[pos];
				pos = row.Index;
				this.Position.Y = pos;
				if (this.CurrentRow.IsCollapsed == true)
					x = 0;
			}
			finally
			{
				this.CropPosition();
				this.LogicalPosition = new TextPoint(x, this.Position.Y);
				this.CropPosition();
				this.CaretMoved(select);
			}
		}

		/// <summary>
		/// Moves the caret down one row.
		/// </summary>
		public void MoveDown(bool select)
		{
			this.CropPosition();
			int x = oldLogicalXPos;

			Row row = this.CurrentRow;
			Row downRow = row.NextVisibleRow;
			if (downRow != null)
			{
				this.Position.Y = downRow.Index;
				if (this.CurrentRow.IsCollapsed == true)
					x = 0;
			}

			this.CropPosition();
			this.LogicalPosition = new TextPoint(x, this.Position.Y);
			this.CropPosition();
			this.CaretMoved(select);
		}

		/// <summary>
		/// Moves the caret left one step.
		/// </summary>
		public void MoveLeft(bool select)
		{
			this.CropPosition();
			this.Position.X--;

			if (this.CurrentRow.IsCollapsedEndPart == true)
			{
				if (this.Position.X < this.CurrentRow.ExpansionStartChar)
				{
					if (this.CurrentRow.ExpansionStartRow.Index == -1)
						Debugger.Break();

					this.Position.Y = this.CurrentRow.ExpansionStartRow.Index;
					this.Position.X = this.CurrentRow.ExpansionStartRow.ExpansionEndChar;
					this.CropPosition();
				}

				this.RememberXPos();
				this.CaretMoved(select);
			}
			else
			{
				if (this.Position.X < 0)
				{
					if (this.Position.Y > 0)
					{
						this.MoveUp(select);
						this.CropPosition();

						Row row = this.CurrentRow;
						this.Position.X = row.Text.Length;
						if (CurrentRow.IsCollapsed)
						{
							this.Position.Y = CurrentRow.ExpansionEndRow.Index;
							this.Position.X = CurrentRow.Text.Length;
						}
					}
					else
						this.CropPosition();
				}

				this.RememberXPos();
				this.CaretMoved(select);
			}
		}

		/// <summary>
		/// Moves the caret to the first non whitespace column at the active row.
		/// </summary>
		public void MoveHome(bool select)
		{
			this.CropPosition();
			if (this.CurrentRow.IsCollapsedEndPart == true)
			{
				this.Position.Y = CurrentRow.ExpansionStartRow.Index;
				this.MoveHome(select);
			}
			else
			{
				int i = this.CurrentRow.GetLeadingWhitespace().Length;
				if (this.Position.X == i)
					this.Position.X = 0;
				else
					this.Position.X = i;

				this.RememberXPos();
				this.CaretMoved(select);
			}
		}

		/// <summary>
		/// Moves the caret to the end of a row, ignoring any whitespace characters at the end of the row.
		/// </summary>
		public void MoveEnd(bool select)
		{
			if (this.CurrentRow.IsCollapsed == true)
			{
				this.Position.Y = CurrentRow.ExpansionEndRow.Index;
				this.MoveEnd(select);
			}
			else
			{
				this.CropPosition();
				Row row = this.CurrentRow;
				this.Position.X = row.Text.Length;

				this.RememberXPos();
				this.CaretMoved(select);
			}
		}

		/// <summary>
		/// Updates the position of the caret.
		/// </summary>
		public void CaretMoved(bool select)
		{
			control.ScrollIntoView();
			if (select == false)
				control.Selection.ClearSelection();
			else
				control.Selection.MakeSelection();
		}

		/// <summary>
		/// Moves the caret to the first column of the active row.
		/// </summary>
		public void MoveAbsoluteHome(bool select)
		{
			this.Position.X = 0;
			this.Position.Y = 0;

			this.RememberXPos();
			this.CaretMoved(select);
		}

		/// <summary>
		/// Moves the caret to the absolute end of the active row.
		/// </summary>
		public void MoveAbsoluteEnd(bool select)
		{
			this.Position.X = control.Document[control.Document.Count - 1].Text.Length;
			this.Position.Y = control.Document.Count - 1;

			this.RememberXPos();
			this.CaretMoved(select);
		}

		/// <summary>
		/// Returns the segment that the caret currently is in.
		/// </summary>
		/// <returns>The segment that the caret currently is in.</returns>
		public Segment CurrentSegment()
		{
			return control.Document.GetSegmentFromPos(this.Position);
		}

		/// <summary>
		/// Sets the position of the caret.
		/// </summary>
		public void SetPosition(TextPoint pos)
		{
			this.Position = pos;
			this.RememberXPos();
		}

		/// <summary>
		/// Confines the caret to a valid position within the active document.
		/// </summary>
		public void CropPosition()
		{
			if (this.Position.X < 0)
				this.Position.X = 0;

			if (this.Position.Y >= control.Document.Count)
				this.Position.Y = control.Document.Count - 1;

			if (this.Position.Y < 0)
				this.Position.Y = 0;

			Row row = this.CurrentRow;
			if (this.Position.X > row.Text.Length && control.VirtualWhitespace == false)
				this.Position.X = row.Text.Length;
		}

		#endregion

		#region Private

		/// <summary>
		/// 'Remembers' the current x coordinate position by storing it in a variable.
		/// </summary>
		private void RememberXPos()
		{
			oldLogicalXPos = this.LogicalPosition.X;
		}

		#endregion

		#region EventHandlers

		private void PositionChange(object s, EventArgs e)
		{
			this.OnChange();
		}

		/// <summary>
		/// Raises the OnChange event.
		/// </summary>
		private void OnChange()
		{
			if (this.Change != null)
				this.Change(this, null);
		}

		#endregion

		#endregion

		/// <summary>
		/// Initializes a new instance of Caret.
		/// </summary>
		public Caret(TextEditorBase control)
		{
			this.Position = new TextPoint(0, 0);
			this.control = control;
		}
	}
}
